#include "math.h"

#include "IndiceHashExtensible.h"
#include "Cuba.h"
#include "DatoCuba.h"
#include "ClaveDatoCuba.h"
#include "Registro.h"
#include "EstrategiaHashSHA1.h"
#include "funciones.h"
#include "Excepciones.h"
#include "Archivo.h"

namespace Toolkit
{

IndiceHashExtensible::IndiceHashExtensible(
        const unsigned long int tamanoComponente, const string &ruta,
        const string &nombre)
{
    this->estrategiaHash = new EstrategiaHashSHA1();
    this->tamanoComponente = tamanoComponente;

    string rutaNueva = ruta + "indiceHashing/";

    stringstream streamVacio;
    this->almacenamientoCubas = new Archivo(tamanoComponente, rutaNueva, string(
            nombre).append(".cuba"), BINARIO, streamVacio);

    Cuba cuba(tamanoComponente, BINARIO);
	unsigned long int posicionRelativaCuba = almacenarCuba(cuba);

	this->directorio = new Directorio(posicionRelativaCuba, rutaNueva, nombre);
}

IndiceHashExtensible::IndiceHashExtensible(const string &ruta,
        const string &nombre)
{
    string rutaNueva = ruta + "indiceHashing/";

    this->estrategiaHash = new EstrategiaHashSHA1();

    this->almacenamientoCubas = new Archivo(rutaNueva,
            string(nombre).append(".cuba"), BINARIO);

    this->tamanoComponente = almacenamientoCubas->getTamano();

    this->directorio = new Directorio(rutaNueva, nombre);
}

IndiceHashExtensible::~IndiceHashExtensible()
{
    if (estrategiaHash != NULL)
    {
        delete estrategiaHash;
    }

    if (almacenamientoCubas != NULL)
    {
        delete almacenamientoCubas;
    }

    if (directorio != NULL)
    {
    	delete directorio;
    }
}

unsigned long int IndiceHashExtensible::crearDireccion(const Clave &clave , int cantidadBits) const
{
    stringstream claveSerializada;
    clave.serializar(claveSerializada);
    string resultadoFuncionHash = estrategiaHash->hash(claveSerializada.str());

    return crearDireccion(resultadoFuncionHash, cantidadBits);
}

unsigned long int IndiceHashExtensible::crearDireccion(
        const string &claveHasheada, int cantidadBits) const
{
	/**
	 * si no se le especifica la cantidad de bits a utilizar,
	 * usa una cantidad de bits igual que la profundidad del directorio.
	 */
	if (cantidadBits == -1)
	{
		cantidadBits = directorio->getProfundidad();
	}

    unsigned long int resultado = 0;

    int cantidadCaracteres = ceil(((double)cantidadBits) / 8);

    for (int i = 0; i < cantidadCaracteres; i++)
    {
        unsigned char caracter = claveHasheada[claveHasheada.size() - i - 1];
        int vector[sizeof(caracter) * 8];
        charToBits(caracter, vector);

        int posicion = 7;
        while ((cantidadBits > 0) and (posicion >= 0))
        {
            resultado = resultado << 1;
            resultado = resultado | vector[posicion];
            cantidadBits--;
            posicion--;
        }
    }
    return resultado;
}

void IndiceHashExtensible::insertar(const Clave &clave,
        const unsigned long int &posicionBloque)
{
    Cuba *cuba = buscarCuba(clave);
    DatoCuba datoCuba(posicionBloque, clave, BINARIO);

    if (cuba->puedeAgregar(datoCuba))
    {
    	Registro *registro = new Registro(BINARIO);
    	registro->setDato(datoCuba);
        ClaveDatoCuba claveDatoCuba(datoCuba, BINARIO);

        cuba->agregar(registro, claveDatoCuba);
        actualizarCubaEnAlmacenamiento(*cuba);
    } else {

    	if (cuba->getProfundidad() == directorio->getProfundidad())
        {
            directorio->expandir();
        }

        unsigned int inicio, fin;
        calcularRangoDirecciones(*cuba, inicio, fin);

        Cuba *nuevaCuba = cuba->dividir(*this);
        actualizarCubaEnAlmacenamiento(*cuba, inicio);

        const unsigned int promedioRango = (inicio + fin) / 2;
        agregarCuba(*nuevaCuba, promedioRango, fin);

        delete nuevaCuba;

        insertar(clave, posicionBloque);
    }
    delete cuba;
}

void IndiceHashExtensible::intentarCombinar(Cuba &cuba,
        long int &direccionDirectorio, const Clave &clave)
{
    long int direccionCompanero = direccionDirectorio;
    bool tieneCompanero = directorio->buscarCompaneroCuba(cuba, direccionCompanero);

    /**
     *  Si la Cuba tiene companero, intenta combinarse con este. Si logra combinarse, el directorio
     *  se intenta contraerDirectorio, en caso de lograrlo el metodo llama recursivamente al metodo
     *  intentarCombinar porque existe la posibilidad de que la Cuba se combine de nuevo.
     */
    if (tieneCompanero)
    {
        Cuba *cubaCompanero = buscarCuba(clave, direccionCompanero);
        bool exitoCombinar = cuba.combinar(*cubaCompanero);
        delete cubaCompanero;

        if (exitoCombinar)
        {
            eliminarCuba(direccionCompanero);

            bool exitoContraer = directorio->intentarContraer();

            if (exitoContraer)
            {
                direccionDirectorio = direccionDirectorio >> 1;
                intentarCombinar(cuba, direccionDirectorio, clave);
            }
        }
    }
}

bool IndiceHashExtensible::eliminar(const Clave &clave)
{
    Cuba *cuba = buscarCuba(clave);
    DatoCuba datoAux(0, clave, BINARIO);
    ClaveDatoCuba claveAux(datoAux, BINARIO);
    Registro *registro = (Registro*) cuba->remover(claveAux);

    //si no existe la clave a eliminar
    if (registro == NULL)
    {
        delete cuba;
        return false;
    }

    long int direccionDirectorio = crearDireccion(clave);

    intentarCombinar(*cuba, direccionDirectorio, clave);
    actualizarCubaEnAlmacenamiento(*cuba, direccionDirectorio);

    delete registro;
    delete cuba;
    return true;
}

bool IndiceHashExtensible::buscar(const Clave & clave,
        unsigned long int &posicionBloque) const
{
    Cuba *cuba = buscarCuba(clave);

    DatoCuba datoAux(0, clave, BINARIO);
    ClaveDatoCuba claveAux(datoAux, BINARIO);

    Registro *registro = (Registro*) cuba->buscar(claveAux);

    if (registro == NULL)
    {
        posicionBloque = 0;
        delete cuba;
        return false;
    }

    DatoCuba *datoCuba = (DatoCuba*)registro->clonarDato();
    posicionBloque = datoCuba->getPosicionBloque();

    delete datoCuba;
    delete cuba;
    return true;
}

Cuba* IndiceHashExtensible::buscarCuba(const Clave &clave,
        long int direccionDirectorio) const
{
    if (direccionDirectorio == -1)
    {
        direccionDirectorio = crearDireccion(clave);
    }

    unsigned long int posicionCuba = directorio->obtenerPosicionCuba(direccionDirectorio);
    stringstream cubaSerializada;
    almacenamientoCubas->recuperarDato(posicionCuba, cubaSerializada);

    Cuba *cuba = new Cuba(tamanoComponente, BINARIO);

    //creo un dato molde para hidratar la cuba
    DatoCuba dato(0, clave, BINARIO);
    cuba->hidratar(cubaSerializada, dato);

    return cuba;
}

void IndiceHashExtensible::calcularRangoDirecciones(const Cuba &cuba,
        unsigned int &comienzo, unsigned int &fin) const
{
    DatoCuba *datoAux = cuba.getDatoCualquiera();

    //Si esta vacia, el metodo el metodo no puede calcular rango y tira excepcion.
    if (datoAux == NULL)
    {
    	throw NoSePuedeCalcularRangoException();
    }

    string claveHasheada = datoAux->getClaveHasheada();
    delete datoAux;

    int direccionClave = crearDireccion(claveHasheada);

    comienzo = direccionClave - (direccionClave % directorio->cantidadRegistros(cuba));
    fin = comienzo + directorio->cantidadRegistros(cuba);
}

unsigned long int IndiceHashExtensible::almacenarCuba(const Cuba &cuba)
{
    stringstream cubaSerializada;
    cuba.serializar(cubaSerializada);

    return almacenamientoCubas->guardarDato(cubaSerializada);
}

void IndiceHashExtensible::agregarCuba(const Cuba &cuba, const unsigned int inicio,
        const unsigned int fin)
{
	unsigned long int posicionNuevaCuba = almacenarCuba(cuba);
	directorio->actualizar(posicionNuevaCuba, inicio, fin);
}

void IndiceHashExtensible::eliminarCuba(const long int &direccionDirectorio)
{
    stringstream cubaSerializada;
    almacenamientoCubas->eliminarDato(directorio->obtenerPosicionCuba(direccionDirectorio),
            cubaSerializada);
    unsigned long int direccionCompanero = direccionDirectorio ^ 1;
    directorio->agregar(directorio->obtenerPosicionCuba(direccionCompanero), direccionDirectorio);
}

void IndiceHashExtensible::actualizarCubaEnAlmacenamiento(const Cuba &cuba,
        long int posicionDirectorio)
{
	stringstream cubaSerializada;
	cuba.serializar(cubaSerializada);

	if ((posicionDirectorio == -1) && (cuba.getProfundidad() > 0))
	{
		unsigned int inicio, fin;
		calcularRangoDirecciones(cuba, inicio, fin);
		almacenamientoCubas->guardarDato(cubaSerializada, directorio->obtenerPosicionCuba(inicio));

	} else if ((posicionDirectorio == -1) && (cuba.getProfundidad() == 0)) {
		almacenamientoCubas->guardarDato(cubaSerializada, directorio->obtenerPosicionCuba(0));

	} else {
		almacenamientoCubas->guardarDato(cubaSerializada, directorio->obtenerPosicionCuba(posicionDirectorio));
	}
}

}
